package com.feather.common.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;

import com.feather.common.text.BytePool;
import com.feather.common.text.CharPool;

/**
 * @author flogyin
 */
public class FileUtil {
	private static final int	BUFFER_SIZE	= 1024;

	/**
	 * 判别在 MS 系统下文件命名是否包含不合法字符.
	 * 
	 * @return int: 非负值表示命名串的索引, 即此处出现不合法字符 -1 表示命名串为零长度, 或者空值 null -2
	 *         表示未找到不合法字符
	 */
	public static int checkMSFileNaming(String n) {
		if (n == null)
			return -1;
		int len = n.length();
		if (len == 0)
			return -1;
		// 文件名不能包括以下字符:
		// \ / : * ? " < > |
		// 39 47 58 42 63 34 60 62 124
		char[] unallowed = new char[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' };
		for (int i = 0; i < len; i++) {
			char achar = n.charAt(i);
			for (int j = 0; j < unallowed.length; j++)
				if (achar == unallowed[j])
					return i;
		}
		return -2;
	}

	public static byte[] readFile(String path) throws IOException {
		if (path == null)
			return null;
		return readFile(new File(path));
	}

	public static byte[] readFile(File path) throws IOException {
		if (path == null) {
			throw new IOException("assign file path is null.");
		}
		if (!path.exists()) {
			throw new IOException("assign file path is not exists.");
		}
		FileInputStream fin = null;
		int len = (int) path.length();
		byte[] buf = new byte[len];
		int length = -1;
		try {
			fin = new FileInputStream(path);
			length = fin.read(buf);
		} finally {
			try {
				if (fin != null)
					fin.close();
			} catch (IOException ex) {
				throw ex;
			}
		}
		return (length != -1) ? buf : null;
	}

	public static byte[] read(InputStream input) throws IOException {
		return read(input, true);
	}

	public static byte[] read(InputStream input, boolean close) throws IOException {
		if (input == null) {
			return null;
		}
		BufferedInputStream bis = new BufferedInputStream(input);
		BytePool readPool = new BytePool();
		try {
			do {
				byte[] buf = new byte[BUFFER_SIZE];
				int len = bis.read(buf);
				if (len > 0) {
					if (len == BUFFER_SIZE) {
						readPool.addReaded(buf);
					} else {
						readPool.addReaded(buf, len);
					}
				} else {
					break;
				}
			} while (true);
		} catch (IOException ex) {
			throw ex;
		} finally {
			if (close) {
				try {
					bis.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
			if (close) {
				try {
					input.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}
		return readPool.totalBytes();
	}

	public static void read(InputStream input, OutputStream output) throws IOException {
		if (input == null || output == null) {
			return;
		}
		BufferedInputStream bis = new BufferedInputStream(input);
		byte[] buf = new byte[BUFFER_SIZE];
		int len = 0;
		while ((len = bis.read(buf)) > 0) {
			output.write(buf, 0, len);
		}
	}

	public static char[] read(Reader reader) throws IOException {
		return read(reader, true);
	}

	public static char[] read(Reader reader, boolean close) throws IOException {
		if (reader == null) {
			return null;
		}
		CharPool readPool = new CharPool();
		try {
			do {
				char[] buf = new char[BUFFER_SIZE];
				int len = reader.read(buf);
				if (len > 0) {
					if (len == BUFFER_SIZE) {
						readPool.addReaded(buf);
					} else {
						readPool.addReaded(buf, len);
					}
				} else {
					break;
				}
			} while (true);
		} catch (IOException ex) {
			throw ex;
		} finally {
			if (close) {
				reader.close();
			}
		}
		return readPool.totalChars();
	}

	public static boolean save2File(String fileName, byte[] bytes) throws IOException {
		if (fileName == null) {
			return false;
		}
		return save2File(new File(fileName), bytes);
	}

	public static boolean save2File(File file, byte[] bytes) throws FileNotFoundException, IOException {
		if (file == null || bytes == null) {
			return false;
		}

		file.getParentFile().mkdirs();

		FileOutputStream fout = null;
		BufferedOutputStream bos = null;
		try {
			fout = new FileOutputStream(file);
			bos = new BufferedOutputStream(fout);
			bos.write(bytes);
		} finally {
			try {
				if (bos != null)
					bos.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			try {
				if (fout != null)
					fout.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		return true;
	}

	public static boolean save2File(String fileName, InputStream input) throws IOException {
		if (fileName == null) {
			return false;
		}
		return save2File(new File(fileName), input);
	}

	public static boolean save2File(File file, InputStream input) throws FileNotFoundException, IOException {
		if (file == null || input == null) {
			return false;
		}

		file.getParentFile().mkdirs();

		FileOutputStream fout = null;
		BufferedOutputStream bos = null;
		try {
			fout = new FileOutputStream(file);
			bos = new BufferedOutputStream(fout);
			byte[] buf = new byte[BUFFER_SIZE];
			int len = 0;
			while ((len = input.read(buf)) > 0) {
				bos.write(buf, 0, len);
			}
		} finally {
			try {
				input.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			try {
				if (bos != null)
					bos.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
			try {
				if (fout != null)
					fout.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		return true;
	}

	/*
	 * Copy file to another file or copy content of a directory to another
	 * directory
	 * 
	 * @inputPath: path to a file or directory
	 * 
	 * @outputPath: path to a file or directory
	 * 
	 * @filteredExtensions: filter files that end with specified strings i.e
	 * {".java", ".class", ".xml"}
	 */
	public static void copy(String inputPath, String outputPath, String[] filteredExtensions) throws IOException {
		File inputPathFile = new File(inputPath);
		if (!inputPathFile.exists()) {
			return;
		}
		File outputPathFile = new File(outputPath);
		if (!outputPathFile.exists()) {
			if (outputPathFile.isDirectory()) {
				outputPathFile.mkdirs();
			} else {
				new File(outputPathFile.getParent()).mkdirs();
			}
		}
		@SuppressWarnings("rawtypes")
		Vector files = findFiles(inputPath, filteredExtensions);
		for (int i = 0; i < files.size(); i++) {
			File in = (File) files.elementAt(i);
			String outFilePath = in.getAbsolutePath().substring(inputPath.length());
			outFilePath = outputPath + File.separator + outFilePath;
			File out = new File(outFilePath);
			File parent = new File(out.getParent());
			if (!parent.exists()) {
				parent.mkdirs();
			}
			copy(new FileInputStream(in), new FileOutputStream(out));
		}
	}

	/*
	 * Copy an input stream to an output stream
	 * 
	 * @in: input stream
	 * 
	 * @out: output stream
	 */
	public static void copy(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[512];
		while (true) {
			int bytesRead = in.read(buffer);
			if (bytesRead == -1) {
				break;
			}
			out.write(buffer, 0, bytesRead);
		}
		in.close();
		out.close();
	}

	/*
	 * Jar a given directory
	 * 
	 * @jarFileName: path of the output jar
	 * 
	 * @jarDirectory: path to the input directory
	 * 
	 * @filteredExtensions: filter files that end with specified strings i.e
	 * {".java", ".class", ".xml"}
	 */
	public static void createJarFromDirectory(String jarFileName, String jarDirectory, String[] filtertedExtensions) throws IOException {
		File directory = new File(jarDirectory);
		if (!directory.exists()) {
			return;
		}
		File jar = new File(jarFileName);
		if (!jar.exists()) {
			new File(jar.getParent()).mkdirs();
		}
		JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(jar), new Manifest());
		@SuppressWarnings("rawtypes")
		Vector files = findFiles(jarDirectory, filtertedExtensions);
		for (int i = 0; i < files.size(); i++) {
			File file = (File) files.elementAt(i);
			String relativePathToDirectory = file.getAbsolutePath().substring(directory.getAbsolutePath().length() + 1);
			String entryName = relativePathToDirectory.replace('\\', '/');
			FileInputStream inStream = new FileInputStream(file);
			ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
			int length = 0;
			byte[] buffer = new byte[BUFFER_SIZE];
			try {
				while ((length = inStream.read(buffer)) > 0) {
					byteStream.write(buffer, 0, length);
				}
			} finally {
				try {
					inStream.close();
				} catch (Exception ex) {
				}
			}
			byte[] arr = byteStream.toByteArray();
			byteStream.close();
			JarEntry meta = new JarEntry(entryName);
			jarOut.putNextEntry(meta);
			meta.setSize(arr.length);
			meta.setCompressedSize(arr.length);
			CRC32 crc = new CRC32();
			crc.update(arr);
			meta.setCrc(crc.getValue());
			meta.setMethod(ZipEntry.STORED);
			jarOut.write(arr, 0, arr.length);
			jarOut.closeEntry();
		}
		jarOut.close();
	}

	/*
	 * Return vector of all Files contained in a path.
	 * 
	 * @filteredExtensions: filter files that end with specified strings i.e
	 * {".java", ".class", ".xml"} If filteredExtensions == null or empty then
	 * then return all instances of File contained in the directory and its sub
	 * directories
	 * 
	 * @Path: a directory path or a file path. If it's file path then return a
	 * single instance of File If it's directory path then return all instances
	 * of File contained in the directory and its sub directories
	 */
	@SuppressWarnings("rawtypes")
	public static Vector findFiles(String path, String[] filteredExtensions) {
		Vector<File> files = new Vector<File>();
		findFilesHelper(new File(path), filteredExtensions, files);
		return files;
	}

	public static List<File> listFiles(String path, String[] filteredExtensions) {
		ArrayList<File> files = new ArrayList<File>();
		findFilesHelper(new File(path), filteredExtensions, files);
		return files;
	}

	/*
	 * INTERNAL: traverse the directory to find all files with filtered
	 * extensions. The result is passed around for each recursive call
	 */
	private static void findFilesHelper(File file, String[] filteredExtensions, Vector<File> result) {
		if (!file.exists()) {
			return;
		}
		if (file.isDirectory()) {
			String[] entries = file.list();
			for (int i = 0; i < entries.length; i++)
				findFilesHelper(new File(file, entries[i]), filteredExtensions, result);
		} else {
			// add everything if no filtered extension
			if ((filteredExtensions == null) || (filteredExtensions.length == 0)) {
				result.addElement(file);
				return;
			}
			// add only filtered extensions
			for (int i = 0; i < filteredExtensions.length; i++) {
				if (file.getName().endsWith(filteredExtensions[i])) {
					result.addElement(file);
					return;
				}
			}
		}
	}

	private static void findFilesHelper(File file, String[] filteredExtensions, List<File> result) {
		if (!file.exists()) {
			return;
		}
		if (file.isDirectory()) {
			String[] entries = file.list();
			for (int i = 0; i < entries.length; i++)
				findFilesHelper(new File(file, entries[i]), filteredExtensions, result);
		} else {
			// add everything if no filtered extension
			if ((filteredExtensions == null) || (filteredExtensions.length == 0)) {
				result.add(file);
				return;
			}
			// add only filtered extensions
			for (int i = 0; i < filteredExtensions.length; i++) {
				if (file.getName().endsWith(filteredExtensions[i])) {
					result.add(file);
					return;
				}
			}
		}
	}

	/*
	 * Delete a file or directory
	 * 
	 * @File: directory or file
	 */
	public static void delete(File file) {
		if (!file.exists()) {
			return;
		}
		if (file.isDirectory()) {
			String[] entries = file.list();
			if (entries.length == 0) {
				file.delete();
			} else {
				for (int i = 0; i < entries.length; i++) {
					delete(new File(file, entries[i]));
				}
				// delete directory after its containing files were deleted
				if (file.list().length == 0)
					file.delete();
			}
		} else {
			file.delete();
		}
	}
}
